import { Easing, Tween, remove } from "@tweenjs/tween.js";
import {
  BlinnPhongMaterial,
  Material,
  Mesh,
  MeshRenderer,
  PrimitiveMesh,
  Script,
  Entity,
  Vector3,
  Quaternion,
} from "oasis-engine";
import { GameConfig } from "./GameConfig";
import { Table } from "./type";
import { Utils } from "./utils";

enum RoleStatus {
  Static,
  Shrink,
  Jump,
  Dead,
}

const gravity = 700;
const upVec = new Vector3(0, 1, 0);
const rotateAxis = new Vector3();
const defaultScale = 1;
const defaultHeadY = 9.5;

export class RoleScript extends Script {
  currentStation: Entity;

  private minScale = 0.8;
  private maxScale = 1.6;
  private groundY = 10;
  private minHeadY = 7.5;
  private widthScaleDelta =
    (this.maxScale - defaultScale) / GameConfig.totalShrinkTime;
  private heightScaleDelta =
    (defaultScale - this.minScale) / GameConfig.totalShrinkTime;
  private translateDelta =
    (defaultHeadY - this.minHeadY) / GameConfig.totalShrinkTime;
  private bodyTranslateDelta =
    GameConfig.rolePressMaxHeight / GameConfig.totalShrinkTime;
  private status: RoleStatus = RoleStatus.Static;
  private bodyEntity: Entity;
  private headEntity: Entity;
  private touchStartTime: number = -1;
  private velocityX: number = 0;
  private velocityY: number = 0;
  private jumpTotalTime: number = 0;
  private jumpTime: number = 0;
  private onJumpComplete: Function[] = [];
  private translateDirection: Vector3 = new Vector3();

  public get size(): number {
    return 4;
  }

  onAwake() {
    const engine = this.engine;
    const material = new BlinnPhongMaterial(engine);
    material.baseColor.setValue(0, 0, 1, 1);

    const headEntity = this.createRolePart(
      "head",
      PrimitiveMesh.createSphere(engine, 1.8),
      material
    );
    headEntity.transform.setPosition(0, defaultHeadY, 0);

    const bodyEntity = this.entity.createChild("body");

    const bottomEntity = this.createRolePart(
      "bottom",
      PrimitiveMesh.createCylinder(engine, 1.2, 2.0, 4, 20),
      material,
      bodyEntity
    );

    bottomEntity.transform.setPosition(0, 2, 0);

    const middleEntity = this.createRolePart(
      "middle",
      PrimitiveMesh.createCylinder(engine, 1.6, 1.2, 2.6, 20),
      material,
      bodyEntity
    );
    middleEntity.transform.setPosition(0, 4.6, 0);

    const middleSphere = this.createRolePart(
      "middleSphere",
      PrimitiveMesh.createSphere(engine, 1.6),
      material,
      bodyEntity
    );

    middleSphere.transform.setScale(1, 0.54, 1);
    middleSphere.transform.setPosition(0, 6, 0);

    this.bodyEntity = bodyEntity;
    this.headEntity = headEntity;
  }

  onUpdate(dt: number) {
    switch (this.status) {
      case RoleStatus.Shrink: {
        const currentScale = this.bodyEntity.transform.scale;

        Utils.scaleY(
          this.bodyEntity,
          -(this.heightScaleDelta * dt) / 1000,
          this.minScale
        );

        Utils.scaleX(
          this.bodyEntity,
          (this.widthScaleDelta * dt) / 1000,
          this.maxScale
        );

        currentScale.z = currentScale.x;

        Utils.translateY(
          this.headEntity,
          -(this.translateDelta * dt) / 1000,
          this.minHeadY
        );

        Utils.translateY(
          this.entity,
          -(5 / 2000) * dt,
          GameConfig.rolePressMaxHeight
        );
        break;
      }
      case RoleStatus.Jump: {
        if (this.jumpTime > this.jumpTotalTime) {
          this.status = RoleStatus.Static;
          const pos = this.entity.transform.position;
          pos.y = this.groundY;
          this.entity.transform.position = pos;
          for (let i = 0; i < this.onJumpComplete.length; i++) {
            this.onJumpComplete[i]();
          }
          return;
        }
        const t = dt / 1000;
        const translateY = this.velocityY * t;
        const translateX = this.velocityX * t;
        this.velocityY = this.velocityY - gravity * t;
        const pos = this.entity.transform.position;
        this.entity.transform.setPosition(
          pos.x + translateX * this.translateDirection.x,
          pos.y + translateY,
          pos.z + translateX * this.translateDirection.z
        );
        this.jumpTime += t;
        break;
      }
    }
  }

  private reBounceTween: Tween<any> = null;
  reBounce() {
    const scale = this.bodyEntity.transform.scale;
    const position = this.headEntity.transform.position;

    this.reBounceTween && remove(this.reBounceTween);

    this.reBounceTween = new Tween({
      scaleX: scale.x,
      scaleY: scale.y,
      scaleZ: scale.z,
      positionY: position.y,
    })
      .to({ scaleX: 1, scaleY: 1, scaleZ: 1, positionY: 9.5 }, 200)
      .onUpdate((obj) => {
        scale.setValue(obj.scaleX, obj.scaleY, obj.scaleZ);
        position.y = obj.positionY;
        this.headEntity.transform.position = position;
        this.bodyEntity.transform.scale = scale;
      })
      .easing(Easing.Elastic.Out)
      .start();
  }

  press() {
    if (this.status === RoleStatus.Static) {
      this.touchStartTime = Date.now();
      this.status = RoleStatus.Shrink;
    }
  }

  release(direction: Vector3) {
    if (this.status === RoleStatus.Shrink) {
      direction.normalize().cloneTo(this.translateDirection);
      this.calculateVelocity(Date.now() - this.touchStartTime);
      this.jumpTime = 0;
      // this.jumpTotalTime = (this.velocityY / gravity) * 2;
      this.jumpTotalTime = this.calculateTotalTime(
        this.velocityY,
        gravity,
        Math.abs(this.entity.transform.position.y)
      );
      this.reBounce();
      this.jumpRotate();
      this.status = RoleStatus.Jump;
    }
  }

  dieVertical(onComplete: Function) {
    this.status = RoleStatus.Dead;
    new Tween({ y: this.groundY })
      .to({ y: -3 }, 800)
      .onUpdate((obj) => {
        const pos = this.entity.transform.position;
        pos.y = obj.y;
        this.entity.transform.position = pos;
      })
      .onComplete(onComplete as any)
      .start();
  }

  dieRotate(table: Table, onComplete: Function) {
    this.status = RoleStatus.Dead;
    const tablePos = table.entity.transform.position;
    const rolePos = this.entity.transform.position;
    rotateAxis.setValue(rolePos.x - tablePos.x, 0, rolePos.z - tablePos.z);
    Vector3.cross(rotateAxis, upVec, rotateAxis);
    const quat = this.entity.transform.rotationQuaternion;
    new Tween({ rotation: 0 })
      .to({ rotation: -120 }, 800)
      .onUpdate((obj, t) => {
        Quaternion.rotationAxisAngle(
          rotateAxis,
          (Math.PI * obj.rotation) / 180,
          quat
        );
        this.entity.transform.rotationQuaternion = quat;
      })
      .onComplete(onComplete as any)
      .start();
  }

  reset() {
    this.entity.transform.setPosition(-15, this.groundY, 0);
    this.entity.transform.setRotation(0, 0, 0);
    this.status = RoleStatus.Static;
  }

  subscribeJumpComplete(callback: Function) {
    this.onJumpComplete.push(callback);
  }

  unsubscribeJumpComplete(callback: Function) {
    const onJumpComplete = this.onJumpComplete;
    const index = onJumpComplete.indexOf(callback);
    if (index > -1) {
      const temp = onJumpComplete[index];
      onJumpComplete[index] = onJumpComplete[onJumpComplete.length - 1];
      onJumpComplete[onJumpComplete.length - 1] = temp;
    }
  }

  private jumpRotate() {
    this.calculateRotateAxis();
    const quat = this.entity.transform.rotationQuaternion;
    new Tween({
      rotation: 0,
    })
      .to({ rotation: -360 }, this.jumpTotalTime * 1000)
      .onUpdate((obj) => {
        Quaternion.rotationAxisAngle(
          rotateAxis,
          (Math.PI * obj.rotation) / 180,
          quat
        );
        this.entity.transform.rotationQuaternion = quat;
      })
      .onComplete(() => {
        this.entity.transform.setRotation(0, 0, 0);
      })
      .easing(Easing.Linear.None)
      .start();
  }

  private createRolePart(
    name: string,
    mesh: Mesh,
    material: Material,
    parent?: Entity
  ) {
    parent = parent ?? this.entity;
    const entity = parent.createChild(name);
    const meshRenderer = entity.addComponent(MeshRenderer);
    meshRenderer.mesh = mesh;
    meshRenderer.setMaterial(material);
    return entity;
  }

  private calculateVelocity(touchDuration: number) {
    this.velocityX = Math.min(touchDuration / 6, 400);
    // this.velocityX = 80;
    this.velocityY = Math.min(150 + touchDuration / 18, 400);
  }

  private calculateRotateAxis() {
    rotateAxis.setValue(
      this.translateDirection.x,
      0,
      this.translateDirection.z
    );
    Vector3.cross(rotateAxis, upVec, rotateAxis);
  }

  private calculateTotalTime(v: number, g: number, h: number) {
    const moreTime = (-v + Math.sqrt(v * v - 2 * g * -h)) / g;
    return (v / g) * 2 - moreTime;
  }
}
